﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

using Fishpark.PdfWriter.Embedding;
using Fishpark.PdfWriter.Utilities;

namespace Fishpark.PdfWriter.Linearisation
{
    /// <summary>
    /// The Inventory class organises a set of PDF object references into lists for pages, document-wide objects and shared objects.
    /// This class is used by the PdfLineariser to separate the source PDF into different parts as per the PDF Linearisation
    /// specification.
    /// </summary>
    internal class Inventory
    {
        // Private fields
        // ==============
        #region Private fields
        private PdfObjectNode _documentRoot;                // The document root object
        private PdfObjectNode _firstPage;                   // The first page to be displayed
        private List<PdfObjectNode> _pages;                 // The pages in the document
        private List<PdfObjectNode> _sharedObjects;         // The shared objects in the document
        private List<PdfObjectNode> _remainingObjects;      // The remaining objects
        #endregion



        // Public properties
        // =================
        #region Public properties
        /// <summary>
        /// Retrieves the document root object.
        /// </summary>
        public PdfObjectNode DocumentRoot
        {
            get
            {
                return _documentRoot;
            }
        }

        /// <summary>
        /// Retrieves the object for the page that is to be displayed first.
        /// </summary>
        public PdfObjectNode FirstPage
        {
            get
            {
                return _firstPage;
            }
        }

        /// <summary>
        /// Retrieves objects for all the pages in the document.
        /// </summary>
        public IEnumerable<PdfObjectNode> Pages
        {
            get
            {
                return _pages;
            }
        }

        /// <summary>
        /// Retrieves the remaining objects in the document.
        /// </summary>
        public IEnumerable<PdfObjectNode> RemainingObjects
        {
            get
            {
                return _remainingObjects;
            }
        }

        /// <summary>
        /// Retrieves the objects that are shared by all pages (not counting ones that are used by
        /// the page that is to be displayed first)
        /// </summary>
        public IEnumerable<PdfObjectNode> SharedObjects
        {
            get
            {
                return _sharedObjects;
            }
        }

        #endregion



        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Parses the specified cross-reference tables to build up an inventory of the source document.
        /// </summary>
        /// <param name="xref">The cross-reference tables</param>
        /// <param name="lexer">The PDF lexer that reads token off the source stream.</param>
        public void ParseCrossReferenceTables(CrossReferenceTables xref, PdfLexer lexer)
        {
            // We build up a list of PDF object nodes. These form the basis for our inventory.
            List<PdfObjectNode> objectNodes = new List<PdfObjectNode>(xref.Count);
            foreach (CrossReferenceEntry entry in xref.Entries)
                if(false==entry.ObjectReference.IsEmpty())
                    objectNodes.Add(new PdfObjectNode(entry.ObjectReference));
                else
                    objectNodes.Add(null);

            // We now go through the entries in the cross-reference and parse their contents using the lexer.
            // We are interested in object references contained within each object. These are the object's children.
            // We need to look for duplicates. A duplicate will be an object that has an index less than the current one, when the
            // cross reference is asked to return the index.
            int objectEnum = 0;
            foreach (CrossReferenceEntry xrefEntry in xref.Entries)
            {
                PdfObjectNode objectNode = objectNodes[objectEnum];
                if(null!=objectNode)
                {
                    // See if this isn't a duplicate.
                    if (objectEnum == xref.FindIndex(objectNode.ObjectReference))
                    {
                        // We parse the object and find child references.
                        // We locate the object node that corresponds to that child, and link it to the object.
                        IEnumerable<PdfObjectReference> children = FindObjectChildren(xrefEntry, lexer);
                        foreach (PdfObjectReference child in children)
                        {
                            int childIndex = xref.FindIndex(child);
                            if (childIndex >= 0)
                                objectNode.Add(objectNodes[childIndex]);
                        }
                    } // if this isn't a duplicate
                } // if this isn't an empty entry

                // next object...
                ++objectEnum;

            } // for each entry in the xref

            // We now have a full tree of objects and their children. We now need to inventarise these into the document root,
            // pages, shared objects and remaining objects.
            InventoriseObjects(objectNodes);

        } // ParseCrossReferenceTables()
        #endregion



        // Private implementation
        // ======================
        #region Private implementation
        /// <summary>
        /// Returns a list of object references that are referred to by the object in the specified cross-reference entry.
        /// </summary>
        /// <param name="xrefEntry">The cross-reference entry to locate the object for.</param>
        /// <param name="lexer">The lexer to use to locate the object within the source stream</param>
        /// <returns>A list of object references</returns>
        private IEnumerable<PdfObjectReference> FindObjectChildren(CrossReferenceEntry xrefEntry, PdfLexer lexer)
        {
            Debug.Assert(false==xrefEntry.ObjectReference.IsEmpty());
            Debug.Assert(null != lexer);

            List<PdfObjectReference> children = new List<PdfObjectReference>();

            // We set the lexer up so that it points to the object.
            // We then read tokens until we find the end of the object or the beginning of a stream
            // We are on the lookout for 'R' tokens. These indicate that the previous two numbers were part of an object
            // reference.
            lexer.Seek(xrefEntry.Location, System.IO.SeekOrigin.Begin);
            bool foundEnd = false;
            Queue<int> queuedNumbers = new Queue<int>();
            while (false==foundEnd && lexer.Read())
            {
                switch (lexer.TokenType)
                {
                    case PdfTokenType.Number:
                        if (2 == queuedNumbers.Count) queuedNumbers.Dequeue();
                        queuedNumbers.Enqueue(ConversionUtilities.ReadInt32(lexer.Value));
                        break;

                    case PdfTokenType.Token:
                        queuedNumbers.Clear();
                        string tokenValue = lexer.Value;
                        if ("stream" == tokenValue || "endobj" == tokenValue)
                            foundEnd = true;
                        break;

                    case PdfTokenType.R:
                        if (2 == queuedNumbers.Count)
                            children.Add(new PdfObjectReference(queuedNumbers.Dequeue(), queuedNumbers.Dequeue()));
                        else
                            queuedNumbers.Clear();
                        break;

                    case PdfTokenType.Whitespace:
                        break;

                    default:
                        queuedNumbers.Clear();
                        break;
                }
            }
            return children;
        } // FindObjectChildren()

        private void FindPageObjects(IEnumerable<PdfObjectNode> objectNodes)
        {
        }

        private void FindSharedObjects(IEnumerable<PdfObjectNode> objectNodes)
        {
            foreach(PdfObjectNode pageNode in _pages)
            {
                MarkPageObjects(pageNode);
            }
        }

        /// <summary>
        /// Inventorises the source document by organising the specified object nodes into a document root, pages, shared objects and
        /// remaining objects.
        /// </summary>
        /// <param name="objectNodes">The list of object nodes to organise</param>
        private void InventoriseObjects(IEnumerable<PdfObjectNode> objectNodes)
        {
            Debug.Assert(null != objectNodes);

            // Find all page nodes
            FindPageObjects(objectNodes);

            // Find all shared objects
            FindSharedObjects(objectNodes);

        }

        private void MarkPageObjects(PdfObjectNode pageNode)
        {
            foreach(PdfObjectNode childNode in pageNode.Children)
            {
                //if(null==childNode.PageNode) childNode.PageNode = pageNode;
                //else _sharedObjects.Add(childNode);
            }
        }
        #endregion



        // Public constructor
        // ==================
        #region Public constructor
        /// <summary>
        /// Constructs a new Inventory instance.
        /// </summary>
        public Inventory()
        {
            _documentRoot = null;
            _firstPage = null;
            _pages = new List<PdfObjectNode>();
            _remainingObjects = new List<PdfObjectNode>();
            _sharedObjects = new List<PdfObjectNode>();
        }
        #endregion
    } // Inventory class
}
